// Copyright © 2022 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package fositex

import (
	"context"
	"crypto/sha512"
	"hash"
	"html/template"
	"net/url"

	"github.com/hashicorp/go-retryablehttp"

	"github.com/ory/hydra/v2/driver/config"
	"github.com/ory/hydra/v2/fosite"
	"github.com/ory/hydra/v2/fosite/compose"
	"github.com/ory/hydra/v2/fosite/i18n"
	"github.com/ory/hydra/v2/fosite/token/jwt"
	"github.com/ory/hydra/v2/oauth2"
	"github.com/ory/hydra/v2/persistence"
	"github.com/ory/hydra/v2/x"
	"github.com/ory/x/stringslice"
	"github.com/ory/x/urlx"
)

type (
	configDependencies interface {
		config.Provider
		persistence.Provider
		x.HTTPClientProvider
		ClientHasher() fosite.Hasher
		ExtraFositeFactories() []Factory
	}
	Factory func(config fosite.Configurator, storage fosite.Storage, strategy interface{}) interface{}
	Config  struct {
		deps configDependencies

		authorizeEndpointHandlers  fosite.AuthorizeEndpointHandlers
		tokenEndpointHandlers      fosite.TokenEndpointHandlers
		tokenIntrospectionHandlers fosite.TokenIntrospectionHandlers
		revocationHandlers         fosite.RevocationHandlers
		deviceEndpointHandlers     fosite.DeviceEndpointHandlers
		jwksFetcherStrategy        fosite.JWKSFetcherStrategy

		*config.DefaultProvider
	}
	ConfigProvider interface {
		OAuth2Config() *Config
	}
)

var (
	defaultResponseModeHandler = fosite.NewDefaultResponseModeHandler()
	defaultFactories           = []Factory{
		compose.OAuth2AuthorizeExplicitFactory,
		compose.OAuth2AuthorizeImplicitFactory,
		compose.OAuth2ClientCredentialsGrantFactory,
		compose.OAuth2RefreshTokenGrantFactory,
		compose.OpenIDConnectExplicitFactory,
		compose.OpenIDConnectHybridFactory,
		compose.OpenIDConnectImplicitFactory,
		compose.OpenIDConnectRefreshFactory,
		compose.OAuth2TokenRevocationFactory,
		compose.OAuth2TokenIntrospectionFactory,
		compose.OAuth2PKCEFactory,
		compose.RFC7523AssertionGrantFactory,
		compose.OIDCUserinfoVerifiableCredentialFactory,
		compose.RFC8628DeviceFactory,
		compose.RFC8628DeviceAuthorizationTokenFactory,
		compose.OpenIDConnectDeviceFactory,
	}
)

func NewConfig(deps configDependencies) *Config {
	return &Config{
		deps:            deps,
		DefaultProvider: deps.Config(),
	}
}

func (c *Config) Config() *config.DefaultProvider {
	return c.deps.Config()
}

func (c *Config) LoadDefaultHandlers(storage fosite.Storage, strategy interface{}) {
	factories := append(defaultFactories, c.deps.ExtraFositeFactories()...)
	for _, factory := range factories {
		res := factory(c, storage, strategy)
		if ah, ok := res.(fosite.AuthorizeEndpointHandler); ok {
			c.authorizeEndpointHandlers.Append(ah)
		}
		if th, ok := res.(fosite.TokenEndpointHandler); ok {
			c.tokenEndpointHandlers.Append(th)
		}
		if tv, ok := res.(fosite.TokenIntrospector); ok {
			c.tokenIntrospectionHandlers.Append(tv)
		}
		if rh, ok := res.(fosite.RevocationHandler); ok {
			c.revocationHandlers.Append(rh)
		}
		if dh, ok := res.(fosite.DeviceEndpointHandler); ok {
			c.deviceEndpointHandlers.Append(dh)
		}
	}
}

func (c *Config) GetJWKSFetcherStrategy(context.Context) fosite.JWKSFetcherStrategy {
	if c.jwksFetcherStrategy == nil {
		c.jwksFetcherStrategy = fosite.NewDefaultJWKSFetcherStrategy(fosite.JWKSFetcherWithHTTPClientSource(
			func(ctx context.Context) *retryablehttp.Client { return c.deps.HTTPClient(ctx) },
		))
	}
	return c.jwksFetcherStrategy
}

func (c *Config) GetHTTPClient(ctx context.Context) *retryablehttp.Client {
	return c.deps.HTTPClient(ctx)
}

func (c *Config) GetAuthorizeEndpointHandlers(context.Context) fosite.AuthorizeEndpointHandlers {
	return c.authorizeEndpointHandlers
}

func (c *Config) GetTokenEndpointHandlers(context.Context) fosite.TokenEndpointHandlers {
	return c.tokenEndpointHandlers
}

func (c *Config) GetTokenIntrospectionHandlers(context.Context) (r fosite.TokenIntrospectionHandlers) {
	return c.tokenIntrospectionHandlers
}

func (c *Config) GetRevocationHandlers(context.Context) fosite.RevocationHandlers {
	return c.revocationHandlers
}

// GetDeviceEndpointHandlers returns the deviceEndpointHandlers
func (c *Config) GetDeviceEndpointHandlers(context.Context) fosite.DeviceEndpointHandlers {
	return c.deviceEndpointHandlers
}

func (c *Config) GetGrantTypeJWTBearerCanSkipClientAuth(context.Context) bool {
	return false
}

func (c *Config) GetAudienceStrategy(context.Context) fosite.AudienceMatchingStrategy {
	return fosite.DefaultAudienceMatchingStrategy
}

func (c *Config) GetOmitRedirectScopeParam(context.Context) bool {
	return false
}

func (c *Config) GetSanitationWhiteList(context.Context) []string {
	return []string{"code", "redirect_uri"}
}

func (c *Config) GetEnablePKCEPlainChallengeMethod(context.Context) bool {
	return false
}

func (c *Config) GetDisableRefreshTokenValidation(context.Context) bool {
	return false
}

func (c *Config) GetRefreshTokenScopes(context.Context) []string {
	return []string{"offline", "offline_access"}
}

func (c *Config) GetMinParameterEntropy(_ context.Context) int {
	return fosite.MinParameterEntropy
}

func (c *Config) GetClientAuthenticationStrategy(context.Context) fosite.ClientAuthenticationStrategy {
	// Fosite falls back to the default fosite.Fosite.DefaultClientAuthenticationStrategy when this is nil.
	return nil
}

func (c *Config) GetResponseModeHandlerExtension(context.Context) fosite.ResponseModeHandler {
	return defaultResponseModeHandler
}

func (c *Config) GetSendDebugMessagesToClients(ctx context.Context) bool {
	return c.deps.Config().GetSendDebugMessagesToClients(ctx)
}

func (c *Config) GetMessageCatalog(context.Context) i18n.MessageCatalog {
	// Fosite falls back to the default messages when this is nil.
	return nil
}

func (c *Config) GetSecretsHasher(context.Context) fosite.Hasher {
	return c.deps.ClientHasher()
}

func (c *Config) GetTokenEntropy(context.Context) int {
	return 32
}

func (c *Config) GetHMACHasher(context.Context) func() hash.Hash {
	return sha512.New512_256
}

func (c *Config) GetIDTokenIssuer(ctx context.Context) string {
	return c.deps.Config().IssuerURL(ctx).String()
}

func (c *Config) GetAllowedPrompts(context.Context) []string {
	return []string{"login", "none", "consent", "registration"}
}

func (c *Config) GetRedirectSecureChecker(context.Context) func(context.Context, *url.URL) bool {
	return x.IsRedirectURISecure(c.deps.Config())
}

func (c *Config) GetAccessTokenIssuer(ctx context.Context) string {
	return c.deps.Config().IssuerURL(ctx).String()
}

func (c *Config) GetJWTScopeField(ctx context.Context) jwt.JWTScopeFieldEnum {
	return c.deps.Config().GetJWTScopeField(ctx)
}

func (c *Config) GetFormPostHTMLTemplate(context.Context) *template.Template {
	return fosite.DefaultFormPostTemplate
}

func (c *Config) GetTokenURLs(ctx context.Context) []string {
	return stringslice.Unique([]string{
		c.deps.Config().OAuth2TokenURL(ctx).String(),
		urlx.AppendPaths(c.deps.Config().PublicURL(ctx), oauth2.TokenPath).String(),
	})
}

// GetDeviceVerificationURL returns the device verification url
func (c *Config) GetDeviceVerificationURL(ctx context.Context) string {
	return urlx.AppendPaths(c.deps.Config().PublicURL(ctx), oauth2.DeviceVerificationPath).String()
}
